var Torrent = function() {

	// Propriétés classiques des torrents
	this.name = ""; // Normalement toujours renseigné
	this.announce = "";
	this.createdBy = "";
	this.creationDateTimestamp = 0;
	this.creationDate = null;
	this.encoding = "";
	this.comment = "";
	// Ces 2 propriétés fournis par le .torrent sont très importantes
	
	this.obj = null;

	// Paramètres optionnel, s'ils sont à null (non renseigné), ne seront pas pris en compte
	this.pieces = null; // Normalement toujours renseigné
	this.pieceLength = null; // Normalement toujours renseigné
	this.length = null; // Taille totale
	this.files = null; // Liste des fichiers
	this.isPrivate = null; // Torrent privé?

	
	var optionnalParams = ['private', 'files', 'pieces', 'piece length']; // Les paramètres optionnels dans l'objet info
	var parsedParams = ['private', 'files', 'pieces', 'piece length', 'name']; // Les paramètres déjà parsés dans l'objet
	var orderParams = []; // Ordre dans le dictionnaire à respecter
	
	this.extraParams = {}; // Les paramètres qui figurent dans le dictionnaire info parsé
	
	var $this = this;
	
	
	
	/**
	 * Remplit l'objet selon un objet obtenu lors du parse d'un fichier torrent, avec TorrentParser
	 */
	this.fromTorrentParserObj = function(obj) {
		$this.announce = obj.announce;
		$this.createdBy = obj["created by"];
		$this.creationDateTimestamp = obj["creation date"];
		
		$this.creationDate = new Date($this.creationDateTimestamp*1000);
		$this.encoding = obj.encoding;
		

		// Nom du fichier à télécharger
		if (PHPJS.isset(obj.info.name)) {
			$this.name = obj.info.name;
		}
		
		if (PHPJS.isset(obj.comment)) {
			$this.comment = obj.comment;
		}		
		
		// Les "pieces" (normalement obligatoire)
		if (PHPJS.isset(obj.info.pieces)) {
			$this.pieces = obj.info.pieces;
		}
		
		// La taille d'une "piece" (normalement obligatoire)
		if (PHPJS.isset(obj.info['piece length'])) {
			$this.pieceLength = obj.info['piece length'];
		}
		
		// La taille de tous les fichiers à télécharger
		if (PHPJS.isset(obj.info.length)) {
			$this.length = obj.info.length;
		}
		
		// Tracker privé?
		if (PHPJS.isset(obj.info["private"])) {
			if (obj.info["private"] == 1) {
				$this.isPrivate = true;
			} else {
				$this.isPrivate = false;
			}
		}
		
		//$this.files = obj.files;
		$this.files = [];
		if (PHPJS.isset(obj.info.files)) {
			for (var i = 0; i < obj.info.files.length; i++) {
				var f = convertFile(obj.info.files[i]);
				$this.files.push(f);
			}
		}
		
		// Liste des fichiers contenu dans le torrent (si plus d'un)
		for (var i in obj.info) {
			orderParams.push(i);
			if (parsedParams.indexOf(i) == -1) {
				$this.extraParams[i] = obj.info[i];
			}
		}
		
		$this.obj = obj;
	};
	
	/**
	 * Remplit l'objet avec le contenu d'un fichier torrent
	 * Si le contenu vient d'un appel AJAX; il faut ajouter cette ligne pour que le contenu du fichier soit obtenu intégralement et correctement:
	 *
	 * var request = new XMLHttpRequest();
	 * request.overrideMimeType("text/plain; charset=x-user-defined"); 
	 *
	 */
	this.fromTorrentFile = function(contents) {
		$this.contents = contents;
		var parser = new TorrentParser();
		var torrentobj = parser.parse(contents);
		$this.fromTorrentParserObj(torrentobj);
	};
	
	/**
	 * Parse une URL envoyant un fichier .torrent
	 * 
	 * TODO: Asynchrone?
	 */
	this.fromTorrentURL = function(url) {
		var request = new XMLHttpRequest();
		request.overrideMimeType("text/plain; charset=x-user-defined"); 
		request.open('GET', url, false);
		request.send(); // because of "false" above, will block until the request is done 
						// and status is available. Not recommended, however it works for simple cases.
		
		var ret = "";
		if (request.status === 200) {
			ret = request.responseText;
			$this.fromTorrentFile(ret);
		}
		
	};
	
	/**
	 * Convertit une occurence du tableau "files" en TorrentFile
	 */
	function convertFile(objf) {
		var f = new TorrentFile();
		f.path = objf.path.join("/");
		f.filename = objf.path[objf.path.length-1];
		f.length = objf.length; 
		return f;
	}
	
	/**
	 * Obtient le dictionnaire "info" avec les propriétés de l'objet courant
	 */
	function getInfoArrayTorrent() {
	
		// Ordre des informations
		var info = {};
		for (var i = 0; i < orderParams.length; i++) {
			info[orderParams[i]] = null;
		}
		
		// Nom du torrent
		info.name = $this.name;
		
		// Liste des fichiers
		if ($this.files != null) {
			info.files = [];
			for (var i = 0; i < $this.files.length; i++) {
				var f = $this.files[i];
				info.files.push({
					length: f.length,
					path: f.path.split("/")
				});
			}
		}
		
		// Tracker privé
		if ($this.isPrivate != null) {
			info['private'] = $this.isPrivate?1:0;
		}
		
		// Taille totale du torrent
		if ($this.length != null) {
			info.length = $this.length;
		}
		
		// Pieces
		if ($this.pieces != null) {
			info.pieces = $this.pieces;
		}
		// piece length
		if ($this.pieceLength != null) {
			info['piece length'] = $this.pieceLength;
		}
		
		// On renseigne tout les paramètres "extra"
		for (var i in $this.extraParams) {
			info[i] = $this.extraParams[i];
		}
		
		// On supprime toute les variables à null
		for (var i in info) {
			if (info[i] == null) {
				delete info[i];
			}
		}
		
		return info;
	}
	
	/**
	 * Obtient le magnetlink correspodant au torrent
	 */
	this.toMagnetLink = function() {
		var url = false;

		var parser = new TorrentParser();
		var info = getInfoArrayTorrent();
		
		// On encode en sha1, car obligatoire. Attention, la fonction sha1 de PHPJS est incorrect pour les string considérés comme un tableau de "bytes"
		// La fonction sha1 a été modifié pour que cela soit possible (et renvoie le même résultat qu'en php)
		
		var infohash = PHPJS.sha1(parser.encode(info), true);
		
		// On encode l'annonce du tracker
		var tr = PHPJS.urlencode($this.announce);
		
		// On encode le fichier avec des %20 pour les espaces, pour que cela s'affiche correctement
		var filename_encoded = PHPJS.rawurlencode($this.name);
		url = "magnet:?xt=urn:btih:"+infohash+"&tr="+tr+"&dn="+filename_encoded;
		
		return url;
	};
	
	/**
	 * Obtient l'objet Blob correspondant au fichier dans son intégralité
	 */
	 
	this.toBlobFile = function() {
		var f = $this.toFile();
		
		// On passe par un Uint8Array pour respecter les bytes
		var byteArray = new Uint8Array(f.length);
		for (var i = 0; i < f.length; i++) {
			byteArray[i] = f.charCodeAt(i) & 0xff;
		}
		var blob = new Blob([byteArray],{type: "application/torrent"});
		return blob;
	};
	
	/**
	 * Obtient le contenu du fichier .torrent correspondant à l'objet Torrent
	 */
	this.toFile = function() {
		var str = "";
		var obj = {
			announce: $this.announce
		};
		if (typeof($this.comment) != "undefined") {
			obj['comment'] = $this.comment;
		}
		if (typeof($this.createdBy) != "undefined") {
			obj['created by'] = $this.createdBy;
		}
		if (typeof($this.creationDateTimestamp) != "undefined") {
			obj['creation date'] = $this.creationDateTimestamp;
		}
		if (typeof($this.encoding) != "undefined") {
			obj['encoding'] = $this.encoding;
		}
		obj.info = getInfoArrayTorrent();
		console.log(obj);
		
		var parser = new TorrentParser();

		return parser.encode(obj);
	};
	
};

/**
 * Fichier contenu dansun torrent
 */
var TorrentFile = function() {
	this.path = ""; // Le chemin complet
	this.filename = ""; // Le nom du fichier
	
	this.length = 0; // Utile uniquement pour la conversion en .torrent. Correspondrait à la taille du fichier
};

/**
 * Classe permettant de parser et encoder un fichier torrent
 * Fonctions reprises des fonctions des classes PHP BEncode et BDecode de 
 */
var TorrentParser = function() {
	// Dictionary keys must be sorted. foreach tends to iterate over the order
	// the array was made, so we make a new one in sorted order. :)
	
	var tempfd = ""; // Pour générer l'encodage en torrent
	
	/**
	 * Tri un tableau associatif (objet)
	 */
	function makeSorted(array)
	{
		var ret = {};
		var keys = [];
		i = 0;
		
		// Shouldn't happen!
		if (PHPJS.empty(array))
			return array;

		for (var key in array) {
			var value = array[key];
			keys[i++] = PHPJS.stripslashes(key);
		}
		PHPJS.sort(keys);
		for (var i=0 ; PHPJS.isset(keys[i]); i++)
			ret[PHPJS.addslashes(keys[i])] = array[PHPJS.addslashes(keys[i])];
		return ret;
	}

	// Encodes strings, integers and empty dictionaries.
	// unstrip is set to true when decoding dictionary keys
	function encodeEntry(entry, unstrip)
	{
		if (typeof(unstrip) == "undefined")
			unstrip = false;
			
		if (PHPJS.is_bool(entry))
		{
			tempfd += "de";
			return;
		}
		if (PHPJS.is_int(entry) || PHPJS.is_float(entry))
		{
			tempfd += "i"+entry+"e";
			return;
		}
		var myentry;
		
		if (unstrip)
			myentry = PHPJS.stripslashes(entry);
		else
			myentry = entry;
		var length = PHPJS.strlen(myentry);
		tempfd += length+":"+myentry;
		return;
	}

	// Encodes lists
	function encodeList(array)
	{

		tempfd += "l";

		// The empty list is defined as array();
		if (PHPJS.empty(array))
		{
			tempfd += "e";
			return;
		}
		for (var i = 0; PHPJS.isset(array[i]); i++) {
			decideEncode(array[i]);
		}
		tempfd += "e";
	}

	// Passes lists and dictionaries accordingly, and has encodeEntry handle
	// the strings and integers.
	function decideEncode(unknown)
	{

		if (typeof(unknown) == "object")
		{
			if (PHPJS.isset(unknown[0]) || PHPJS.empty(unknown))
				return encodeList(unknown);
			else
				return encodeDict(unknown);
		}
		encodeEntry(unknown);
	}
	
	// Encodes dictionaries
	function encodeDict(array)
	{
		tempfd += "d";
		if (PHPJS.is_bool(array))
		{
			tempfd += "e";
			return;
		}
		// NEED TO SORT!
		var newarray = makeSorted(array);
		for (var left in newarray)
		{
			var right = newarray[left];
			encodeEntry(left, true);
			decideEncode(right);
		}
		tempfd += "e";
		return;
	}
	
	// Decode un nombre
	function numberdecode(wholefile, start)
	{
		var ret = [];
		ret[0] = 0;
		var offset = start;

		// Funky handling of negative numbers and zero
		negative = false;
		if (wholefile[offset] == '-')
		{
			negative = true;
			offset++;
		}
		if (wholefile[offset] == '0')
		{
			offset++;
			if (negative)
				return [false];
			if (wholefile[offset] == ':' || wholefile[offset] == 'e')
			{
				offset++;
				ret[0] = 0;
				ret[1] = offset;
				return ret;
			}
			return [false];
		}
		while (true)
		{

			if (wholefile[offset] >= '0' && wholefile[offset] <= '9')
			{
				
				ret[0] *= 10;
				ret[0] += PHPJS.ord(wholefile[offset]) - PHPJS.ord("0");
				offset++;
			}
			// Tolerate : or e because this is a multiuse function
			else if (wholefile[offset] == 'e' || wholefile[offset] == ':')
			{
				ret[1] = offset+1;
				if (negative)
				{
					if (ret[0] == 0)
						return [false];
					ret[0] = - ret[0];
				}
				return ret;
			}
			else {
				return [false];
			}
		}

	}
	// var tmp = "";
	// Decode une entrée
	function decodeEntry(wholefile, offset)
	{
		if (typeof(offset) == "undefined")
			offset = 0;
		
		var ret = [];
		if (wholefile[offset] == 'd') {
			// console.log(tmp+"(dict)");
			return decodeDict(wholefile, offset);
		}
		if (wholefile[offset] == 'l') {
			return decodelist(wholefile, offset);
		}
		if (wholefile[offset] == "i")
		{
			offset++;
			return numberdecode(wholefile, offset);
		}
		// String value: decode number, then grab substring
		var info = numberdecode(wholefile, offset);
		if (info[0] === false) {
			return [false];
		}
		var ret = [];
		ret[0] = PHPJS.substr(wholefile, info[1], info[0]);
		ret[1] = info[1]+PHPJS.strlen(ret[0]);
		return ret;
	}
	
	// Decode une liste
	function decodeList(wholefile, start)
	{

		var offset = start+1;
		var i = 0;
		if (wholefile[start] != 'l')
			return [false];
		var ret = [];
		// tmp += "--";
		while (true)
		{
			if (wholefile[offset] == 'e')
				break;
			value = decodeEntry(wholefile, offset);
			if (value[0] === false)
				return [false];
			// console.log(tmp+"["+i+"] "+value[0]);
			ret[i] = value[0];
			offset = value[1];
			i ++;
		}

		// The empy list is an empty array. Seems fine.
		var returnVal = {};
		returnVal[0] = ret;
		returnVal[1] = offset+1;
		// tmp = tmp.substring(0,2);
		return returnVal;



	}

	// Tries to construct an array
	function decodeDict(wholefile, start)
	{
		if (typeof(start) == "undefined")
			start = 0;
			
		var offset = start;
		if (wholefile[offset] == 'l')
			return decodeList(wholefile, start);
		if (wholefile[offset] != 'd')
			return false;
		var ret = {};
		offset++;
		// tmp += "--";
		while (true)
		{	
			if (wholefile[offset] == 'e')
			{
				offset++;
				break;
			}
			var left = decodeEntry(wholefile, offset);
			if (!left[0])
				return false;
			offset = left[1];
			if (wholefile[offset] == 'd')
			{
				// Recurse
				// console.log(tmp+left[0]+" (dict)");
				var value = decodeDict(wholefile, offset);
				if (!value[0])
					return false;
				ret[PHPJS.addslashes(left[0])] = value[0];
				offset= value[1];
				continue;
			}
			else if (wholefile[offset] == 'l')
			{
				// console.log(tmp+left[0]+" (list)");
				var value = decodeList(wholefile, offset);
				if (!value[0] && is_bool(value[0]))
					return false;
				ret[PHPJS.addslashes(left[0])] = value[0];
				offset = value[1];
				
			}
			else
			{
				// console.log(tmp+left[0]+" (str or number)");
				var value = decodeEntry(wholefile, offset);
				if (value[0] === false)
					return false;
				ret[PHPJS.addslashes(left[0])] = value[0];
				offset = value[1];
				
			}
		}
		var returnVal = [];
		if (PHPJS.empty(ret))
			returnVal[0] = true;
		else
			returnVal[0] = ret;
		returnVal[1] = offset;
		// tmp = tmp.substring(0,2);
		return returnVal;

	}
	
	/**
	 * Parse un fichier .torrent et renvoie un objet avec les propriétés correspondant au format .torrent
	 * Voir: http://fileformats.wikia.com/wiki/Torrent_file
	 */
	this.parse = function(str) {
		var decoded = decodeEntry(str);
		if (decoded != false) {
			return decoded[0];
		}
		return false;
	};
	
	/**
	 * Encode un objet en format .torrent
	 */
	this.encode = function(array) {
		tempfd = "";
		decideEncode(array);
		return tempfd;
	};

};
